﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Xml.Linq;
using System.Text;
using System.Windows.Controls;

namespace Silverlight
{
    /// <summary>
    /// DataRow state
    /// </summary>
    public enum DataRowState
    {
        Added,
        Deleted,
        Detached,
        Modified,
        Unchanged
    }

    /// <summary>
    /// DataRow version
    /// </summary>
    public enum DataRowVersion
    {
        Current,
        Default,
        Original,
        Proposed
    }

    /// <summary>
    /// Provides DataRow functionality
    /// </summary>
    public class DataRow : INotifyPropertyChanged
    {
        #region Data Fields
        /// <summary>
        /// Contains fields for current version
        /// </summary>
        private DataFieldCollection _fields;

        /// <summary>
        /// "Index" property data field
        /// </summary>
        private int _index;

        /// <summary>
        /// "InEditMode" property data field
        /// </summary>
        private bool _inEditMode;

        /// <summary>
        /// "Locked" property data field
        /// </summary>
        private bool _locked;

        /// <summary>
        /// "RowErrors" property data field
        /// </summary>
        private ObservableCollection<DataField> _rowErrors;

        /// <summary>
        /// "RowState" property data field
        /// </summary>
        private DataRowState _rowState;

        /// <summary>
        /// "RowVersion" property data field
        /// </summary>
        private DataRowVersion _rowVersion;

        /// <summary>
        /// "Table" property data filed
        /// </summary>
        private DataTable _table;
        #endregion

        #region Events
        /// <summary>
        /// Value is changing inside column
        /// </summary>
        public event EventHandler<DataSetEventArgs> FieldValueChanging;

        /// <summary>
        /// INotifyPropertyChanged interface implementation
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Row need to be removed
        /// </summary>
        internal event EventHandler<EventArgs> RowDeleted;
        #endregion

        #region Constructors
        /// <summary>
        /// Default constructor
        /// </summary>
        private DataRow()
        {
            this._rowState = DataRowState.Detached;
            this._rowVersion = DataRowVersion.Proposed;
            this._fields = new DataFieldCollection();
            this._rowErrors = new ObservableCollection<DataField>();
        }

        /// <summary>
        /// Makes DataRow instance  with specified number of DataColumn
        /// </summary>
        /// <param name="table">Data table</param>
        internal DataRow(DataTable table) : this()
        {
            // Validate argument
            if (table == null)
            {
                throw new ArgumentNullException("table");
            }

            this._table = table;

            // Initialize fields
            for (int i = 0; i < this._table.Columns.Count; i++)
            {
                AddDataField(i);
            }
        }
        #endregion

        #region Properties
        /// <summary>
        /// Gets "HasErrors" flag value
        /// </summary>
        public bool HasErrors
        {
            get
            {
                return this._rowErrors.Count > 0;
            }
        }

        /// <summary>
        /// Gets "HasNoErrors" flag value
        /// </summary>
        public bool HasNoErrors
        {
            get
            {
                return this._rowErrors.Count == 0;
            }
        }

        /// <summary>
        /// Gets or sets row index
        /// </summary>
        internal int Index
        {
            get
            {
                return this._index;
            }
            set
            {
                // Validate argument
                if (value < 0)
                {
                    throw new ArgumentOutOfRangeException("value");
                }

                this._index = value;
            }
        }

        /// <summary>
        /// Gets value which indicates whether DataRow is in Edit mode
        /// </summary>
        internal bool InEditMode
        {
            get
            {
                return this._inEditMode;
            }
        }
        
        /// <summary>
        /// Gets array of fields for current version
        /// </summary>
        public DataFieldCollection ItemArray
        {
            get
            {
                return this._fields;
            }
        }

        /// <summary>
        /// Gets or sets lock on DataRow
        /// </summary>
        internal bool Locked
        {
            get
            {
                return this._locked;
            }
            set
            {
                this._locked = value;
            }
        }

        /// <summary>
        /// Gets row errors
        /// </summary>
        public ObservableCollection<DataField> RowErrors
        {
            get
            {
                return this._rowErrors;
            }
        }

        /// <summary>
        /// Gets or sets row state
        /// </summary>
        internal DataRowState RowState
        {
            get
            {
                return this._rowState;
            }
            set
            {
                this._rowState = value;
            }
        }

        /// <summary>
        /// Gets or sets row verion
        /// </summary>
        internal DataRowVersion RowVersion
        {
            get
            {
                return this._rowVersion;
            }
            set
            {
                this._rowVersion = value;
            }
        }

        /// <summary>
        /// Gets DataTable to which DataRow belongs
        /// </summary>
        internal DataTable Table
        {
            get
            {
                return this._table;
            }
        }

        /// <summary>
        /// Gets generated type id
        /// </summary>
        internal string TypeId
        {
            get
            {
                // Create type id as concatenation of table name and all column names
                StringBuilder stringBuilder = new StringBuilder();
                stringBuilder.Append(this._table.TableName);
                for (int i = 0; i < this._fields.Count; i++)
                {
                    stringBuilder.Append(this._fields[i].ColumnName);
                }

                return stringBuilder.ToString();
            }
        }

        /// <summary>
        /// Gets xml data
        /// </summary>
        internal XElement XmlData
        {
            get
            {
                XElement rowData = new XElement(this._table.TableName);
                XElement fieldData = null;
                for (int i = 0; i < this._fields.Count; i++)
                {
                    if (this._fields[i].Value != null)
                    {
                        fieldData = new XElement(this._fields[i].ColumnName);
                        fieldData.Value = this._fields[i].Value;
                        rowData.Add(fieldData);
                    }
                }

                return rowData;
            }
        }
        #endregion

        #region Indexers
        /// <summary>
        /// Gets or sets data stored in column with specified index
        /// </summary>
        /// <param name="columnIndex">Column index</param>
        /// <returns>Column value</returns>
        public string this[int columnIndex]
        {
            get
            {
                return this._fields[columnIndex].Value;
            }
            set
            {
                this._fields[columnIndex].Value = value;
            }
        }

        /// <summary>
        /// Gets data stored in column with specified index
        /// Returned value depends on "version" parameter
        /// </summary>
        /// <param name="columnIndex">Column index</param>
        /// <param name="version">Row version</param>
        /// <returns>Column value</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1023:IndexersShouldNotBeMultidimensional")]
        public string this[int columnIndex, DataRowVersion version]
        {
            get
            {
                switch (version)
                {
                    case DataRowVersion.Current:
                        return this._fields[columnIndex].CurrentValue;
                    case DataRowVersion.Default:
                        switch (this._rowState)
                        {
                            case DataRowState.Added:
                                return this._fields[columnIndex].CurrentValue;
                            case DataRowState.Deleted:
                                throw new InvalidOperationException("Can't get value from deleted row");
                            case DataRowState.Detached:
                                return this._fields[columnIndex].ProposedValue;
                            case DataRowState.Modified:
                                return this._fields[columnIndex].CurrentValue;
                            case DataRowState.Unchanged:
                                return this._fields[columnIndex].OriginalValue;
                        }

                        throw new InvalidOperationException("Unrecognized row state");
                    case DataRowVersion.Original:
                        return this._fields[columnIndex].OriginalValue;
                    case DataRowVersion.Proposed:
                        return this._fields[columnIndex].ProposedValue;
                }

                throw new InvalidOperationException("Unrecognized row version");
            }
        }

        /// <summary>
        /// Gets or sets data stored in column with specified name
        /// </summary>
        /// <param name="columnName">Column name</param>
        /// <returns>Column value</returns>
        public string this[string columnName]
        {
            get
            {
                for (int i = 0; i < this._fields.Count; i++)
                {
                    if (this._fields[i].ColumnName == columnName)
                    {
                        return this._fields[i].Value;
                    }
                }

                throw new InvalidOperationException("Column doesn't exist");
            }
            set
            {
                for (int i = 0; i < this._fields.Count; i++)
                {
                    if (this._fields[i].ColumnName == columnName)
                    {
                        this._fields[i].Value = value;
                        return;
                    }
                }

                throw new InvalidOperationException("Column doesn't exist");
            }
        }

        /// <summary>
        /// Gets data stored in column with specified name
        /// </summary>
        /// <param name="columnName">Column name</param>
        /// <param name="version">Row version</param>
        /// <returns>Column value</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1023:IndexersShouldNotBeMultidimensional")]
        public string this[string columnName, DataRowVersion version]
        {
            get
            {
                for (int i = 0; i < this._fields.Count; i++)
                {
                    if (this._fields[i].ColumnName == columnName)
                    {
                        switch (version)
                        {
                            case DataRowVersion.Current:
                                return this._fields[i].CurrentValue;
                            case DataRowVersion.Default:
                                switch (this._rowState)
                                {
                                    case DataRowState.Added:
                                        return this._fields[i].CurrentValue;
                                    case DataRowState.Deleted:
                                        throw new InvalidOperationException("Can't get value from deleted row");
                                    case DataRowState.Detached:
                                        return this._fields[i].ProposedValue;
                                    case DataRowState.Modified:
                                        return this._fields[i].CurrentValue;
                                    case DataRowState.Unchanged:
                                        return this._fields[i].OriginalValue;
                                }

                                throw new InvalidOperationException("Unrecognized row state");
                            case DataRowVersion.Original:
                                return this._fields[i].OriginalValue;
                            case DataRowVersion.Proposed:
                                return this._fields[i].ProposedValue;
                        }

                        throw new InvalidOperationException("Unrecognized row version");
                    }
                }

                throw new InvalidOperationException("Column doesn't exist");
            }
        }
        #endregion

        #region Methods
        /// <summary>
        /// Commits all the changes made since the last time AcceptChanges was called
        /// </summary>
        public void AcceptChanges()
        {
            this._rowState = DataRowState.Unchanged;
            this._rowVersion = DataRowVersion.Original;

            this._inEditMode = false;
            for (int i = 0; i < this._fields.Count; i++)
            {
                this._fields[i].AcceptChanges();
            }
        }

        /// <summary>
        /// Adds DataField to DataFieldCollection
        /// </summary>
        /// <param name="index">DataField index</param>
        /// <returns>Added DataField</returns>
        internal DataField AddDataField(int index)
        {
            DataField dataField = new DataField(index, this);
            dataField.PropertyChanged += new PropertyChangedEventHandler(DataField_PropertyChanged);
            dataField.FieldValueChanging += new EventHandler<DataSetEventArgs>(DataField_FieldValueChanging);

            // Assign default value
            if (this._table.Columns[index].DefaultValue != null)
            {
                dataField.Value = this._table.Columns[index].DefaultValue;
            }

            this._fields.Add(dataField);
            return dataField;
        }

        /// <summary>
        /// Starts edit operation
        /// </summary>
        public void BeginEdit()
        {
            if (!this._inEditMode)
            {
                this._inEditMode = true;

                // Change RowState from Unchanged to Modified
                if (this._rowState == DataRowState.Unchanged)
                {
                    this._rowState = DataRowState.Modified;
                    this._rowVersion = DataRowVersion.Current;
                }
            }
        }

        /// <summary>
        /// Cancels edit operation
        /// </summary>
        public void CancelEdit()
        {
            for (int i = 0; i < this._fields.Count; i++)
            {
                this._fields[i].CancelEdit();
            }

            this._inEditMode = false;
        }

        /// <summary>
        /// Copies DataRow
        /// </summary>
        /// <param name="table">Data table</param>
        internal void Copy(DataTable table)
        {
            // Create DataRow
            DataRow dataRow = table.NewRow();

            // Copy DataFields
            for (int i = 0; i < this._fields.Count; i++)
            {
                this._fields[i].Copy(dataRow._fields[i]);
            }

            // Add DataRow to DataTable
            table.Rows.Add(dataRow);

            // Copy data
            dataRow._inEditMode = this._inEditMode;
            dataRow._rowErrors = this._rowErrors;
            dataRow._rowState = this._rowState;
            dataRow._rowVersion = this._rowVersion;
        }

        /// <summary>
        /// Deletes row
        /// </summary>
        public void Delete()
        {
            if (this._rowState == DataRowState.Added)
            {
                this.OnRowDeleted();
            }
            else
            {
                this.RowState = DataRowState.Deleted;
            }
        }

        /// <summary>
        /// Deletes row
        /// <param name="physicalDeletion">True for physical deletion</param>
        /// </summary>
        internal void Delete(bool physicalDeletion)
        {
            if (physicalDeletion)
            {
                this.OnRowDeleted();
            }
            else
            {
                this.RowState = DataRowState.Deleted;
            }
        }

        /// <summary>
        /// Ends edit operation
        /// </summary>
        public void EndEdit()
        {
            for (int i = 0; i < this._fields.Count; i++)
            {
                this._fields[i].EndEdit();
            }

            this._inEditMode = false;
        }

        /// <summary>
        /// Generates bindable object
        /// </summary>
        /// <param name="connector">Connector instance</param>
        /// <returns>Generated bindable object</returns>
        public object GetBindableData(Connector connector)
        {
            return connector.GetBindableData(this);
        }

        /// <summary>
        /// Gets value from specified column
        /// </summary>
        /// <param name="columnName">Column name</param>
        /// <param name="value">Output value</param>
        [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters")]
        public void GetValue(string columnName, out bool? value)
        {
            // Validate arguments
            if (String.IsNullOrEmpty(columnName))
            {
                throw new ArgumentNullException("columnName");
            }

            // Validate object existence
            DataColumn dataColumn = this._table.Columns[columnName];
            if (dataColumn == null)
            {
                throw new ArgumentOutOfRangeException("columnName");
            }

            if (string.IsNullOrEmpty(this[columnName]))
            {
                value = null;
            }
            else
            {
                bool fieldValue;
                if (!bool.TryParse(this[columnName], out fieldValue))
                {
                    throw new InvalidCastException("value");
                }

                value = fieldValue;
            }
        }

        /// <summary>
        /// Gets value from specified column
        /// </summary>
        /// <param name="columnName">Column name</param>
        /// <param name="value">Output value</param>
        [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters")]
        public void GetValue(string columnName, out DateTime? value)
        {
            // Validate arguments
            if (String.IsNullOrEmpty(columnName))
            {
                throw new ArgumentNullException("columnName");
            }

            // Validate object existence
            DataColumn dataColumn = this._table.Columns[columnName];
            if (dataColumn == null)
            {
                throw new ArgumentOutOfRangeException("columnName");
            }

            if (string.IsNullOrEmpty(this[columnName]))
            {
                value = null;
            }
            else
            {
                DateTime fieldValue;
                if (!DateTime.TryParse(this[columnName], out fieldValue))
                {
                    throw new InvalidCastException("value");
                }

                value = fieldValue;
            }
        }

        /// <summary>
        /// Gets value from specified column
        /// </summary>
        /// <param name="columnName">Column name</param>
        /// <param name="value">Output value</param>
        [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters")]
        public void GetValue(string columnName, out int? value)
        {
            // Validate arguments
            if (String.IsNullOrEmpty(columnName))
            {
                throw new ArgumentNullException("columnName");
            }

            // Validate object existence
            DataColumn dataColumn = this._table.Columns[columnName];
            if (dataColumn == null)
            {
                throw new ArgumentOutOfRangeException("columnName");
            }

            if (string.IsNullOrEmpty(this[columnName]))
            {
                value = null;
            }
            else
            {
                int fieldValue;
                if (!int.TryParse(this[columnName], out fieldValue))
                {
                    throw new InvalidCastException("value");
                }

                value = fieldValue;
            }
        }

        /// <summary>
        /// Gets value from specified column
        /// </summary>
        /// <param name="columnName">Column name</param>
        /// <param name="value">Output value</param>
        [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters")]
        public void GetValue(string columnName, out long? value)
        {
            // Validate arguments
            if (String.IsNullOrEmpty(columnName))
            {
                throw new ArgumentNullException("columnName");
            }

            // Validate object existence
            DataColumn dataColumn = this._table.Columns[columnName];
            if (dataColumn == null)
            {
                throw new ArgumentOutOfRangeException("columnName");
            }

            if (string.IsNullOrEmpty(this[columnName]))
            {
                value = null;
            }
            else
            {
                long fieldValue;
                if (!long.TryParse(this[columnName], out fieldValue))
                {
                    throw new InvalidCastException("value");
                }

                value = fieldValue;
            }
        }

        /// <summary>
        /// Gets value from specified column
        /// </summary>
        /// <param name="columnName">Column name</param>
        /// <param name="value">Output value</param>
        [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters")]
        public void GetValue(string columnName, out string value)
        {
            // Validate arguments
            if (String.IsNullOrEmpty(columnName))
            {
                throw new ArgumentNullException("columnName");
            }

            // Validate object existence
            DataColumn dataColumn = this._table.Columns[columnName];
            if (dataColumn == null)
            {
                throw new ArgumentOutOfRangeException("columnName");
            }

            value = this[columnName];
        }

        /// <summary>
        /// Raises PropertChanged event
        /// </summary>
        /// <param name="propertyName">Property name</param>
        private void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        /// <summary>
        /// Raises RowDeleted event
        /// </summary>
        private void OnRowDeleted()
        {
            if (RowDeleted != null)
            {
                RowDeleted(this, new EventArgs());
            }
        }

        /// <summary>
        /// Recalculates row errors
        /// </summary>
        private void RecalculateRowErrors()
        {
            int errorCount = this._rowErrors.Count;
            this._rowErrors.Clear();
            for (int i = 0; i < this._fields.Count; i++)
            {
                if (this._fields[i].HasError)
                {
                    this._rowErrors.Add(this._fields[i]);
                }
            }

            if (this._rowErrors.Count != errorCount)
            {
                this.OnPropertyChanged("HasErrors");
                this.OnPropertyChanged("HasNoErrors");
            }
        }

        /// <summary>
        /// Rejects all changes made since AcceptChanges was last called
        /// </summary>
        public void RejectChanges()
        {
            this._rowState = DataRowState.Unchanged;
            DataField dataField = this._fields["RowState"];
            if (dataField != null)
            {
                dataField.Value = this._rowState.ToString();
                dataField.AcceptChanges();
            }

            this._rowVersion = DataRowVersion.Original;

            this._inEditMode = false;
            for (int i = 0; i < this._fields.Count; i++)
            {
                this._fields[i].RejectChanges();
            }
        }
        
        /// <summary>
        /// Removed DataField from DataFieldCollection
        /// </summary>
        /// <param name="fieldName">Field name for removing</param>
        internal void Remove(string fieldName)
        {
            DataField dataField = this._fields[fieldName];
            dataField.PropertyChanged -= DataField_PropertyChanged;
            dataField.FieldValueChanging -= DataField_FieldValueChanging;
            this._fields.Remove(dataField);
        }

        /// <summary>
        /// Sets value into filed
        /// </summary>
        /// <param name="columnIndex">Column index</param>
        /// <param name="value">Value for assigning</param>
        internal void SetValue(int columnIndex, object value)
        {
            if (value == null)
            {
                this[columnIndex] = null;
            }
            else
            {
                if (value.GetType().FullName.Contains("DateTime"))
                {
                    this[columnIndex] = string.Format("{0:yyyy-MM-ddThh:mm:ss}", value);
                }
                else
                {
                    this[columnIndex] = value.ToString();
                }
            }
        }

        /// <summary>
        /// Sets value into specified filed
        /// </summary>
        /// <param name="columnName">Column name</param>
        /// <param name="value">Value for assigning</param>
        public void SetValue(string columnName, object value)
        {
            if (value == null)
            {
                this[columnName] = null;
            }
            else
            {
                if (value.GetType().FullName.Contains("DateTime"))
                {
                    this[columnName] = string.Format("{0:yyyy-MM-ddThh:mm:ss}", value);
                }
                else
                {
                    this[columnName] = value.ToString();
                }
            }
        }
        #endregion

        #region Event Handlers
        /// <summary>
        /// Handles DataField.FieldValueChanging event
        /// </summary>
        /// <param name="sender">Event source</param>
        /// <param name="e">Event arguments</param>
        private void DataField_FieldValueChanging(object sender, DataSetEventArgs e)
        {
            if (!this._locked)
            {
                // Recalculate row errors
                this.RecalculateRowErrors();

                // Put row in Edit mode
                this.BeginEdit();

                // Raise FieldValueChanging event
                DataField dataField = (DataField)sender;
                e.Column = this._table.Columns[dataField.Index];
                e.Row = this;
                if (FieldValueChanging != null)
                {
                    FieldValueChanging(this, e);
                }
            }
        }

        /// <summary>
        /// Handles DataField.PropertyChanged event
        /// </summary>
        /// <param name="sender">Event source</param>
        /// <param name="e">Event arguments</param>
        void DataField_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, e);
            }
        }
        #endregion
    }
}
